/*
 * Use of this source code is governed by the MIT license that can be
 * found in the LICENSE file.
 */

package org.rust.cargo.runconfig.legacy

import com.intellij.execution.DefaultExecutionResult
import com.intellij.execution.RunContentExecutor
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.execution.process.CapturingProcessAdapter
import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.runners.AsyncProgramRunner
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.showRunContent
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsContexts.DialogTitle
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.Promise
import org.rust.RsBundle
import org.rust.cargo.runconfig.*
import org.rust.cargo.runconfig.buildtool.CargoBuildManager.getBuildConfiguration
import org.rust.cargo.runconfig.buildtool.CargoBuildManager.isBuildConfiguration
import org.rust.cargo.runconfig.buildtool.CargoBuildManager.isBuildToolWindowAvailable
import org.rust.cargo.runconfig.command.CargoCommandConfiguration
import org.rust.cargo.runconfig.target.localBuildArgsForRemoteRun
import org.rust.cargo.toolchain.CargoCommandLine
import org.rust.cargo.toolchain.impl.CargoMetadata
import org.rust.cargo.toolchain.impl.CompilerArtifactMessage
import org.rust.cargo.toolchain.tools.Cargo.Companion.getCargoCommonPatch
import org.rust.cargo.toolchain.wsl.RsWslToolchain
import org.rust.cargo.util.CargoArgsParser.Companion.parseArgs
import org.rust.openapiext.JsonUtils.tryParseJsonObject
import org.rust.openapiext.saveAllDocuments
import org.rust.stdext.unwrapOrThrow
import java.nio.file.Path
import java.nio.file.Paths

/**
 * This runner is used if [isBuildToolWindowAvailable] is false.
 */
abstract class RsAsyncRunner(
    private val executorId: String,
    @Suppress("UnstableApiUsage") @DialogTitle private val errorMessageTitle: String
) : AsyncProgramRunner<RunnerSettings>() {
    override fun canRun(executorId: String, profile: RunProfile): Boolean {
        if (executorId != this.executorId || profile !is CargoCommandConfiguration ||
            profile.clean() !is CargoCommandConfiguration.CleanConfiguration.Ok) return false
        return !profile.hasRemoteTarget &&
            !profile.isBuildToolWindowAvailable &&
            !isBuildConfiguration(profile) &&
            getBuildConfiguration(profile) != null
    }

    override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> {
        saveAllDocuments()

        state as CargoRunStateBase

        val commandLine = state.prepareCommandLine(getCargoCommonPatch(environment.project))
        val (commandArguments, executableArguments) = parseArgs(commandLine.command, commandLine.additionalArguments)
        val additionalBuildArgs = state.runConfiguration.localBuildArgsForRemoteRun

        val isTestRun = commandLine.command in listOf("test", "bench")
        val cmdHasNoRun = "--no-run" in commandLine.additionalArguments
        val buildCommand = if (isTestRun) {
            if (cmdHasNoRun) commandLine else commandLine.prependArgument("--no-run")
        } else {
            commandLine.copy(command = "build", additionalArguments = commandArguments + additionalBuildArgs)
        }.copy(withSudo = false) // building does not require root privileges

        val getRunCommand = { executablePath: Path ->
            with(commandLine) {
                state.toolchain.createGeneralCommandLine(
                    executablePath,
                    workingDirectory,
                    redirectInputFrom,
                    backtraceMode,
                    environmentVariables,
                    executableArguments,
                    false, // emulateTerminal
                    withSudo,
                    patchToRemote = false // patching is performed for debugger/profiler/valgrind on CLion side if needed
                )
            }
        }

        return buildProjectAndGetBinaryArtifactPath(environment.project, buildCommand, state, isTestRun)
            .then { binary ->
                if (isTestRun && cmdHasNoRun) return@then null
                val path = binary?.path ?: return@then null
                val runCommand = getRunCommand(path)
                getRunContentDescriptor(state, environment, runCommand)
            }
    }

    open fun getRunContentDescriptor(
        state: CargoRunStateBase,
        environment: ExecutionEnvironment,
        runCommand: GeneralCommandLine
    ): RunContentDescriptor? = showRunContent(executeCommandLine(state, runCommand, environment), environment)

    private fun executeCommandLine(
        state: CargoRunStateBase,
        commandLine: GeneralCommandLine,
        environment: ExecutionEnvironment
    ): DefaultExecutionResult = state.executeCommandLine(commandLine, environment)

    open fun checkToolchainConfigured(project: Project): Boolean = true

    open fun checkToolchainSupported(project: Project, host: String): BuildResult.ToolchainError? = null

    open fun processUnsupportedToolchain(
        project: Project,
        toolchainError: BuildResult.ToolchainError,
        promise: AsyncPromise<Binary?>
    ) {
        project.showErrorDialog(toolchainError.message)
        promise.setResult(null)
    }

    private fun buildProjectAndGetBinaryArtifactPath(
        project: Project,
        command: CargoCommandLine,
        state: CargoRunStateBase,
        isTestBuild: Boolean
    ): Promise<Binary?> {
        val promise = AsyncPromise<Binary?>()
        val toolchain = state.toolchain
        val cargo = state.cargo()

        val processForUserOutput = ProcessOutput()
        val commandLine = cargo.toColoredCommandLine(project, command)
        LOG.debug("Executing Cargo command: `${commandLine.commandLineString}`")
        val processForUser = RsProcessHandler(commandLine)

        processForUser.addProcessListener(CapturingProcessAdapter(processForUserOutput))

        invokeLater {
            if (!checkToolchainConfigured(project)) {
                promise.setResult(null)
                return@invokeLater
            }

            RunContentExecutor(project, processForUser)
                .apply { createFilters(state.cargoProject).forEach { withFilter(it) } }
                .withAfterCompletion {
                    if (processForUserOutput.exitCode != 0) {
                        promise.setResult(null)
                        return@withAfterCompletion
                    }

                    object : Task.Backgroundable(project, RsBundle.message("progress.title.building.cargo.project")) {
                        var result: BuildResult? = null

                        override fun run(indicator: ProgressIndicator) {
                            indicator.isIndeterminate = true
                            val host = state.rustVersion()?.host.orEmpty()
                            result = checkToolchainSupported(project, host)
                            if (result != null) return

                            val jsonCommand = command.prependArgument("--message-format=json").copy(emulateTerminal = false)
                            val processForJson = RsCapturingProcessHandler.startProcess(
                                cargo.toGeneralCommandLine(project, jsonCommand)
                            ).unwrapOrThrow()
                            processForJson.setHasPty(toolchain is RsWslToolchain)
                            val output = processForJson.runProcessWithProgressIndicator(indicator)
                            if (output.isCancelled || output.exitCode != 0) {
                                promise.setResult(null)
                                return
                            }

                            result = output.stdoutLines.asSequence()
                                .mapNotNull { tryParseJsonObject(it) }
                                .mapNotNull { CompilerArtifactMessage.fromJson(it) }
                                .filter { (_, target, profile) ->
                                    val isSuitableTarget = when (target.cleanKind) {
                                        CargoMetadata.TargetKind.BIN -> true
                                        CargoMetadata.TargetKind.EXAMPLE -> {
                                            // TODO: support cases when crate types list contains not only binary
                                            target.cleanCrateTypes.singleOrNull() == CargoMetadata.CrateType.BIN
                                        }
                                        CargoMetadata.TargetKind.TEST, CargoMetadata.TargetKind.BENCH -> true
                                        CargoMetadata.TargetKind.LIB -> profile.test
                                        else -> false
                                    }
                                    isSuitableTarget && (!isTestBuild || profile.test)
                                }
                                .flatMap { it.executables.asSequence() }
                                .let { BuildResult.Binaries(it.toList()) }
                        }

                        override fun onSuccess() {
                            when (val result = result!!) {
                                is BuildResult.ToolchainError -> {
                                    processUnsupportedToolchain(project, result, promise)
                                }
                                is BuildResult.Binaries -> {
                                    val binaries = result.paths
                                    when {
                                        binaries.isEmpty() -> {
                                            project.showErrorDialog(RsBundle.message("dialog.message.can.t.find.binary"))
                                            promise.setResult(null)
                                        }
                                        binaries.size > 1 -> {
                                            project.showErrorDialog(
                                                RsBundle.message("dialog.message.more.than.one.binary.was.produced.please.specify.bin.lib.test.or.example.flag.explicitly")
                                            )
                                            promise.setResult(null)
                                        }
                                        else -> promise.setResult(Binary(Paths.get(binaries.single())))
                                    }
                                }
                            }
                        }

                        override fun onThrowable(error: Throwable) = promise.setResult(null)
                    }.queue()
                }.run()
        }

        return promise
    }

    protected fun Project.showErrorDialog(@Suppress("UnstableApiUsage") @NlsContexts.DialogMessage message: String) {
        Messages.showErrorDialog(this, message, errorMessageTitle)
    }

    companion object {
        class Binary(val path: Path)

        private val LOG: Logger = logger<RsAsyncRunner>()
    }
}
